// -----------------------------------------------------------------------------
// <copyright file="Program.cs" company="Justin Wallace Saunders">
//     Copyright (c) Microsoft Public License. All rights reserved.
// </copyright>
// <author name="Justin Wallace Saunders" email="justinsa@live.ca" />
// -----------------------------------------------------------------------------
namespace Community.Tools.Database.Builder.Task
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using Microsoft.Build.Utilities;

    /// <summary>
    /// Builder Task class.
    /// </summary>
    public class Builder : Task
    {
        /// <summary>
        /// Gets or sets the database executable to use for script execution.
        /// This value is set to sqlcmd.exe if no value is provided.
        /// </summary>
        public string DatabaseExecutable { get; set; }

        /// <summary>
        /// Gets or sets the database executable arguments.
        /// This value is set to <code>-E -S %DatabaseServer% -i {0}</code> if no value is provided.
        /// </summary>
        public string DatabaseExecutableArguments { get; set; }

        /// <summary>
        /// Gets or sets the database server to execute scripts against.
        /// This value is set to <code>./SQLEXPRESS</code> if no value is provided.
        /// </summary>
        public string DatabaseServer { get; set; }

        /// <summary>
        /// Gets or sets the individual script execution timeout.
        /// This value is set to 3000 milliseconds if an illegal value is provided.
        /// </summary>
        public int ExecutionTimeout { get; set; }

        /// <summary>
        /// Gets or sets the list of file tails to pre-process against (semi-colon delimited).
        /// </summary>
        public string FileTails { get; set; }

        /// <summary>
        /// Gets or sets the list of script files (semi-colon delimited).
        /// This value is mandatory.
        /// </summary>
        public string Scripts { get; set; }

        /// <summary>
        /// Gets or sets the list for key-value substitutions (semi-colon delimited).
        /// </summary>
        public string Substitutions { get; set; }

        /// <summary>
        /// Executes the task.
        /// </summary>
        /// <returns>True if the task executed without errors, false otherwise</returns>
        public override bool Execute()
        {
            this.Log.LogMessage("Execute Database Builder MSBuild Task");
            Settings settings = new Settings(
                this.Log,
                this.DatabaseExecutable,
                this.DatabaseExecutableArguments,
                this.DatabaseServer,
                this.ExecutionTimeout,
                this.FileTails,
                this.Scripts,
                this.Substitutions);

            if (settings.Scripts == null || settings.Scripts.Count == 0)
            {
                this.Log.LogMessage("Aborting execution as no scripts were provided");
                return true;
            }

            bool result = true;
            foreach (string script in settings.Scripts)
            {
                if (!File.Exists(script))
                {
                    this.Log.LogWarning("Skipping script that does not exist: {0}", script);
                    continue;
                }

                string path = script;
                if (MatchFileTail(script, settings.FileTails))
                {
                    this.Log.LogMessage("Pre-processing script: {0}", script);
                    string contents = File.ReadAllText(script);
                    contents = Substitute(contents, settings.Substitutions);
                    path = Path.GetTempFileName();
                    File.WriteAllText(path, contents);
                }

                if (!ExecuteScriptFile(path, settings))
                {
                    result = false;
                    this.Log.LogError("Script failed to execute properly: {0}", path);
                }
            }

            return result;
        }

        /// <summary>
        /// Performs content substitution based on key-value pairs.
        /// </summary>
        /// <param name="contents">The content to pattern-match</param>
        /// <param name="substitutions">The dictionary of key-value pairs</param>
        /// <returns>The modified contents after all substitutions performed</returns>
        private static string Substitute(string contents, IDictionary<string, string> substitutions)
        {
            if (substitutions == null || substitutions.Count() == 0)
            {
                return contents;
            }

            foreach (var substitution in substitutions)
            {
                contents = contents.Replace(substitution.Key, substitution.Value);
            }

            return contents;
        }

        /// <summary>
        /// Compares a filename to a list of tails to determine a match.
        /// </summary>
        /// <param name="filename">The filename</param>
        /// <param name="tails">File tail list</param>
        /// <returns>True if the filename ends with one of the tails, false otherwise</returns>
        private static bool MatchFileTail(string filename, IEnumerable<string> tails)
        {
            if (tails == null || tails.Count() == 0)
            {
                return false;
            }

            foreach (string tail in tails)
            {
                if (filename.EndsWith(tail, StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Executes a script file using the SQL command line.
        /// </summary>
        /// <param name="file">Script file path</param>
        /// <param name="settings">Task execution settings</param>
        /// <returns>True if the script file executed completely without errors or timing out, false otherwise</returns>
        private bool ExecuteScriptFile(string file, Settings settings)
        {
            bool result = true;
            this.Log.LogMessage("Execute script: {0}", file);
            var startInfo = new ProcessStartInfo();
            startInfo.FileName = settings.DatabaseExecutable;
            startInfo.Arguments = string.Format(CultureInfo.InvariantCulture, settings.DatabaseExecutableArguments, file);
            startInfo.RedirectStandardError = true;
            startInfo.RedirectStandardOutput = true;
            startInfo.UseShellExecute = false;
            startInfo.CreateNoWindow = true;
            this.Log.LogMessage("{0} {1}", startInfo.FileName, startInfo.Arguments);
            using (Process command = new Process())
            {
                command.StartInfo = startInfo;
                command.Start();
                command.WaitForExit(settings.ExecutionTimeout);
                if (!command.HasExited)
                {
                    command.Kill();
                    this.Log.LogError("Killed: {0}", startInfo.FileName);
                    result = false;
                }

                this.Log.LogMessage(command.StandardOutput.ReadToEnd());
                if (command.ExitCode != 0)
                {
                    this.Log.LogError(command.StandardError.ReadToEnd());
                    result = false;
                }
            }

            return result;
        }
    }
}